package com.leps.nq.tx.core;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.BetweenFormatter;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.leps.nq.bu.entity.BuPlc;
import com.leps.nq.bu.entity.BuPump;
import com.leps.nq.bu.entity.BuWell;
import com.leps.nq.bu.service.IBuPlcService;
import com.leps.nq.bu.service.IBuPumpService;
import com.leps.nq.bu.service.IBuWellService;
import com.leps.nq.common.exception.PlcOfflineException;
import com.leps.nq.common.exception.PlcPwdException;
import com.leps.nq.op.entity.OpAlarm;
import com.leps.nq.op.service.IOpAlarmService;
import com.leps.nq.tx.entity.PumpEffectivePoint;
import com.leps.nq.tx.entity.ReadHistory;
import com.leps.nq.tx.entity.WriteRetry;
import com.leps.nq.tx.service.IPumpEffectivePointService;
import com.leps.nq.tx.service.IReadHistoryService;
import com.leps.nq.tx.service.IWriteRetryService;
import com.leps.nq.tx.vo.PlcPumpPointVo;
import com.leps.nq.tx.vo.PumpDataVo;
import com.leps.nq.tx.vo.PumpPointVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Component
@Slf4j
public class PlcReadWriteService {
    @Autowired
    IPumpEffectivePointService pumpEffectivePointService;
    @Autowired
    IBuPumpService pumpService;
    @Autowired
    IBuPlcService plcService;
    @Autowired
    IOpAlarmService alarmService;
    @Autowired
    IReadHistoryService readHistoryService;
    @Autowired
    IBuWellService wellService;
    @Autowired
    IWriteRetryService writeRetryService;

    private Date PREV_READ_TIME;
    private Date READ_TIME;

    public void writeRetry() {
        List<WriteRetry> list = this.writeRetryService.list();
        for (WriteRetry writeRetry : list) {
            int pumpId = writeRetry.getPumpId();
            PumpPointVo pumpPointVo = this.pumpEffectivePointService.selectPumpPointVoByPumpId(pumpId);
            int dataAddress = writeRetry.getDataAddress();
            String dataType = writeRetry.getDataType();
            int dataValue = writeRetry.getDataValue();
            boolean success = this.write(pumpPointVo, dataAddress, dataType, dataValue);
            if (success) {
                this.cancelWriteRetry(writeRetry);
            } else {
                int plcReconnectTimeout = SysConfigs.plcReconnectTimeout();
                long actualTimeout = DateUtil.between(writeRetry.getCreateTime(), new Date(), DateUnit.MINUTE);
                if (actualTimeout >= plcReconnectTimeout) {
                    this.cancelWriteRetry(writeRetry);
                    this.genPlcOfflineAlarm(pumpPointVo.getPlcId());
                }
            }
        }
    }

    private void cancelWriteRetry(WriteRetry writeRetry) {
        writeRetry.setDelFlag(true);
        this.writeRetryService.updateById(writeRetry);
    }

    public boolean write(PumpPointVo vo, int dataAddress, String dataType, int dataValue) {
        final int plcId = vo.getPlcId();
        final int pumpId = vo.getPumpId();
        WriteRequest request = new WriteRequest(vo.getIp(), vo.getPcsPort(), vo.getPcsPwd(), vo.getFirstAddress(), dataType, dataAddress, dataValue);
        try {
            request.getResponse();
            // TODO log
            return true;
        } catch (PlcOfflineException e) {
            e.printStackTrace();
            WriteRetry retry = new WriteRetry();
            retry.setPumpId(pumpId);
            retry.setDataValue(dataValue);
            retry.setCreateTime(new Date());
            retry.setDataAddress(dataAddress);
            retry.setDataType(dataType);
            retry.setDelFlag(false);
            this.writeRetryService.save(retry);
        } catch (PlcPwdException e) {
            e.printStackTrace();
            this.genPlcPwdAlarm(plcId);
        }
        return false;
    }

    public void fetchAllPlc() {
        List<PlcPumpPointVo> plcPumpPointVoList = this.pumpEffectivePointService.selectAllPlcPumpPointVo();
        for (PlcPumpPointVo plcPumpPointVo : plcPumpPointVoList) {
            this.doFetch(plcPumpPointVo);
        }
    }

    public void fetchOnePlc(int pumpId) {
        BuPump pump = this.pumpService.getById(pumpId);
        final int plcId = pump.getPlcId();
        PlcPumpPointVo plcPumpPointVo = this.pumpEffectivePointService.selectPlcPumpPointVoByPlcId(plcId);
        this.doFetch(plcPumpPointVo);
    }

    public void doFetch(PlcPumpPointVo plcPumpPointVo) {
        BuPlc plc = plcService.getById(plcPumpPointVo.getPlcId());
        this.PREV_READ_TIME = plc.getLastReadTime();
        // Create a PLC fetch request
        Integer plcId = plcPumpPointVo.getPlcId();
        String ip = plcPumpPointVo.getIp();
        Integer port = plcPumpPointVo.getPcsPort();
        String pwd = plcPumpPointVo.getPcsPwd();
        int firstAddress = plcPumpPointVo.getFirstAddress();
        ReadRequest request = new ReadRequest(ip, port, pwd);
        // Generate serial number
        long preSerialNumber = this.readHistoryService.getLastSerialNumberByPlcId(plcId);
        final long serialNumber = preSerialNumber + 1;
        log.debug("PLC FETCH REQUEST: {}", request.getDatagramHexStr());
        try {
            Response response = request.getResponse();
            log.debug("PLC FETCH RESPONSE: {}", response.getHexStr());
            // Refresh READ_TIME
            this.refreshReadTime();
            // set plc online
            this.setPlcOnlineStatus(plcId, 0);
            // set plc last read time if success
            this.setPlcLastReadTime(plcId);
            // TAG: handle response with strategy patterns
            if (response == null) return;
            List<PlcPumpPointVo.Pump> pumps = plcPumpPointVo.getPumps();
            byte[] datagram = response.getDatagram();
            for (PlcPumpPointVo.Pump pump : pumps) {
                final int pumpId = pump.getPumpId();
                // STRATEGY: choose pump data handler
                IPumpDataHandler handler = this.choosePumpDataHandler();
                // Map data to PumpDataVo
                List<PumpEffectivePoint> effectivePoints = pump.getEffectivePoints();
                PumpDataVo dataVo = handler.mapDatagramToPumpDataVo(datagram, pumpId, effectivePoints, firstAddress);
                System.out.println(new ObjectMapper().writeValueAsString(dataVo));
                // Save Pump Data to database
                this.saveReadHistory(serialNumber, plcId, pumpId, dataVo, effectivePoints);
                // Alarm if neccessary
                handleAlarm(pump, pumpId, dataVo);
                // update runningStatus, accumulated_running_time of pump
                handleOnlineStatusAndAccumulatedRunningTimeOfPump(dataVo);
                // update current_pump_injection_speed, current_level of relational wells
                updateCurrentLevelAndCurrentPumpInjectionSpeedOfWells(plcId, pumpId, dataVo);
            }
        } catch (PlcOfflineException e) {
            e.printStackTrace();
            // set plc offline
            this.setPlcOnlineStatus(plcId, 1);
            // generate Alarm
            this.genPlcOfflineAlarm(plcId);
        } catch (PlcPwdException e) {
            e.printStackTrace();
            // Generate alarm
            this.genPlcPwdAlarm(plcId);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void updateCurrentLevelAndCurrentPumpInjectionSpeedOfWells(Integer plcId, int pumpId, PumpDataVo dataVo) {
        List<BuWell> wells = wellService.lambdaQuery().eq(BuWell::getPumpId, pumpId).list();
        if (ObjectUtil.isNotEmpty(wells)) {
            Double originLiquidLevel = wells.get(0).getCurrentLevel();
            double currentLevel = dataVo.getRatioLiquidLevel();
            for (BuWell well : wells) {
                well.setCurrentLevel(currentLevel);
                // 顺便更新一下井的开关状态和泵保持同步
                well.setOnoffStatus(dataVo.getRunning() == 0 ? 1 : 0);
            }
//            // 井->当前泵注速度
//            Double currentPumpInjectionSpeed = null;
//            if (originLiquidLevel != null && this.PREV_READ_TIME != null) {
//                long duration = DateUtil.between(this.PREV_READ_TIME, this.READ_TIME, DateUnit.MINUTE);
//                currentPumpInjectionSpeed = (currentLevel - originLiquidLevel) / duration;
//                if (!(currentPumpInjectionSpeed.isNaN() || currentPumpInjectionSpeed.isInfinite())) {
//                    for (BuWell well : wells) {
//                        well.setCurrentPumpInjectionSpeed(currentPumpInjectionSpeed);
//                    }
//                }
//            }
            wellService.updateBatchById(wells);
        }
    }

    private void handleOnlineStatusAndAccumulatedRunningTimeOfPump(PumpDataVo dataVo) {
        String accumulatedRunningTime = DateUtil.formatBetween(dataVo.getOpenRealMinutes() * 60 * 1000, BetweenFormatter.Level.MINUTE);
        BuPump pumpEntity = new BuPump();
        pumpEntity.setPumpId(dataVo.getPumpId());
        pumpEntity.setAccumulatedRunningTime(accumulatedRunningTime);
        pumpEntity.setOnlineStatus(dataVo.getRunning() == 0 ? 1 : 0);
        pumpService.updateById(pumpEntity);
    }

    private void handleAlarm(PlcPumpPointVo.Pump pump, int pumpId, PumpDataVo dataVo) {
        String pumpName = pump.getPumpName();
        double ratioLiquidLevel = dataVo.getRatioLiquidLevel();
        double ratioPressure = dataVo.getRatioPressure();
        int liquidLevelLowerLimit = SysConfigs.liquidLevelLowerLimit();
        int liquidLevelUpperLimit = SysConfigs.liquidLevelUpperLimit();
        int pressureLowerLimit = SysConfigs.pressureLowerLimit();
        int pessureUpperLimit = SysConfigs.pessureUpperLimit();
        OpAlarm liquidAlarm = null;
        if (ratioLiquidLevel < liquidLevelLowerLimit) {
            String alarmValue = AlarmFactory.tooLowLiquidLevelAlarmValue(pumpName, ratioLiquidLevel);
            liquidAlarm = AlarmFactory.createLiquidLevelAlarm(pumpId, alarmValue);
        } else if (ratioLiquidLevel > liquidLevelUpperLimit) {
            String alarmValue = AlarmFactory.tooHighLiquidLevelAlarmValue(pumpName, ratioLiquidLevel);
            liquidAlarm = AlarmFactory.createLiquidLevelAlarm(pumpId, alarmValue);
        }
        if (liquidAlarm != null) {
            alarmService.save(liquidAlarm);
        }
        OpAlarm pressureAlarm = null;
        if (ratioPressure < pressureLowerLimit) {
            String alarmValue = AlarmFactory.tooLowPressureAlarmValue(pumpName, ratioPressure);
            pressureAlarm = AlarmFactory.createPressureAlarm(pumpId, alarmValue);
        } else if (ratioPressure > pressureLowerLimit) {
            String alarmValue = AlarmFactory.tooHighPressureAlarmValue(pumpName, ratioPressure);
            pressureAlarm = AlarmFactory.createPressureAlarm(pumpId, alarmValue);
        }
        if (pressureAlarm != null) {
            alarmService.save(pressureAlarm);
        }
    }

    private void saveReadHistory(Long serialNumber, int plcId, int pumpId, PumpDataVo pumpDataVo, List<PumpEffectivePoint> effectivePoints) {
        List<ReadHistory> readHistories = new ArrayList<>();
        for (int i = 0; i < effectivePoints.size(); i++) {
            PumpEffectivePoint point = effectivePoints.get(i);
            ReadHistory rh = new ReadHistory();
            rh.setPlcId(plcId);
            rh.setPumpId(pumpId);
            rh.setPointId(point.getPepId());
            rh.setSerialNumber(serialNumber);
            rh.setReadTime(this.READ_TIME);
            rh.setDataValue(Convert.toDouble(pumpDataVo.getDataList().get(i)));
            readHistories.add(rh);
        }
        this.readHistoryService.saveBatch(readHistories);
    }

    private void genPlcPwdAlarm(int plcId) {
        OpAlarm alarm = AlarmFactory.createPlcPwdAlarm(plcId);
        this.alarmService.save(alarm);
    }

    private void setPlcLastReadTime(Integer plcId) {
        BuPlc plc = new BuPlc();
        plc.setPlcId(plcId);
        plc.setLastReadTime(this.READ_TIME);
        this.plcService.updateById(plc);
    }

    private void genPlcOfflineAlarm(Integer plcId) {
        OpAlarm alarm = AlarmFactory.createPlcOfflineAlarm(plcId, "system");
        this.alarmService.save(alarm);
    }

    private void setPlcOnlineStatus(Integer plcId, int status) {
        BuPlc plc = new BuPlc();
        plc.setPlcId(plcId);
        plc.setOnlineStatus(status);
        this.plcService.updateById(plc);
    }

    private IPumpDataHandler choosePumpDataHandler() {
        return new DefaultPumpDataHandler();
    }

    public void refreshReadTime() {
        this.READ_TIME = new Date();
    }

}
